// SPDX-FileCopyrightText: 2019-2023 Giovanni Dante Grazioli <deroad@libero.it>
// SPDX-License-Identifier: BSD-3-Clause

import Base from '../core/base.js';
import Variable from '../core/variable.js';

const _REG = {
	X: 'x',
	Y: 'y',
	stackptr: 'SP',
	accumulator: 'acc',
	carry: 'C',
	overflow: 'V',
	decimal: 'D',
	interrupt: 'I',
	zero: 'Z',
};

function _arg(instr, idx) {
	var o = instr.parsed.opd;
	if (idx > 0) {
		return o[idx];
	}
	o = o[0];
	if (o.immediate) {
		return o.token;
	}
	return Variable.pointer(o.token, 16, false);
}

function _math(op, instr, bits) {
	var srcA = _arg(instr, 0);
	var srcB = _arg(instr, 1);
	if (!srcB) {
		return op(_REG.accumulator, _REG.accumulator, srcA, bits);
	}
	return Base.composed([
		op(_REG.accumulator, _REG.accumulator, srcA, bits),
		Base.add(_REG.accumulator, _REG.accumulator, srcB),
	]);
}

function _math_rotate(op, instr, bits) {
	var srcA = _arg(instr, 0);
	var srcB = _arg(instr, 1);
	if (!srcB) {
		// destination, source_a, source_b, bits
		return op(_REG.accumulator, srcA, bits, 16);
	}
	return Base.composed([
		op(_REG.accumulator, srcA, bits, 16),
		Base.add(_REG.accumulator, _REG.accumulator, srcB),
	]);
}

function _load(instr, register, bits) {
	bits = bits || 16;
	register = register || _REG.accumulator;
	var srcA = _arg(instr, 0);
	var srcB = _arg(instr, 1);
	var ops = [];
	if (typeof srcA == "string") {
		// pointer, register, bits, is_signed
		ops.push(Base.read_memory(register, register, srcA, bits));
	} else {
		ops.push(Base.assign(register, srcA));
	}
	if (!srcB) {
		return ops[0];
	}
	ops.push(Base.add(register, register, srcB));
	return Base.composed(ops);
}

function _store(instr, register, bits) {
	bits = bits || 16;
	register = register || _REG.accumulator;
	var srcA = _arg(instr, 0);
	var srcB = _arg(instr, 1);
	var ops = [];
	if (typeof srcA == "string") {
		// pointer, register, bits, is_signed
		ops.push(Base.write_memory(register, register, srcA, bits));
	} else {
		ops.push(Base.assign(srcA, register));
	}
	if (!srcB) {
		return ops[0];
	}
	ops.push(Base.add(register, register, srcB));
	return Base.composed(ops);
}

function _nop(instr, context, instructions) {
	return Base.nop();
}

export default {
	instructions: {
		adc: function(instr) {
			return _math(Base.add, instr);
		},
		and: function(instr) {
			return _math(Base.and, instr);
		},
		asl: function(instr) {
			return _math(Base.shift_left, instr, 1);
		},
		bit: function(instr, context) {
			var p = _arg(instr, 0);
			context.cond.a = '(' + _REG.accumulator + ' & ' + p + ')';
			context.cond.b = '0';
			return Base.nop();
		},
		bcc: function(instr, context) {
			instr.conditional(_REG.carry, 0, 'EQ');
			return Base.nop();
		},
		bcs: function(instr, context) {
			instr.conditional(_REG.carry, 0, 'NE');
			return Base.nop();
		},
		beq: function(instr, context) {
			instr.conditional(context.cond.a, context.cond.b, 'EQ');
			return Base.nop();
		},
		bmi: function(instr, context) {
			instr.conditional(context.cond.a, context.cond.b, 'LT');
			return Base.nop();
		},
		bne: function(instr, context) {
			instr.conditional(context.cond.a, context.cond.b, 'NE');
			return Base.nop();
		},
		bpl: function(instr, context) {
			instr.conditional(context.cond.a, context.cond.b, 'GT');
			return Base.nop();
		},
		brk: function(instr, context) {
			Global().context.addMacro('#define INTERRUPT() __asm(brk)');
			return Base.call(Variable.macro('INTERRUPT'), []);
		},
		bvc: function(instr, context) {
			instr.conditional(_REG.overflow, 0, 'EQ');
			return Base.nop();
		},
		bvs: function(instr, context) {
			instr.conditional(_REG.overflow, 0, 'NE');
			return Base.nop();
		},
		clc: function(instr, context) {
			return Base.assign(_REG.carry, '0');
		},
		cld: function(instr, context) {
			return Base.assign(_REG.decimal, '0');
		},
		cli: function(instr, context) {
			return Base.assign(_REG.interrupt, '0');
		},
		clv: function(instr, context) {
			return Base.assign(_REG.overflow, '0');
		},
		cmp: function(instr, context) {
			instr.setBadJump();
			context.cond.a = _REG.accumulator;
			var srcA = _arg(instr, 0);
			var srcB = _arg(instr, 1);
			if (!srcB) {
				context.cond.b = srcA;
				return Base.nop();
			}
			var v = Variable.uniqueName();
			context.cond.b = v;
			return Base.add(v, srcA, srcB);
		},
		cpx: function(instr, context) {
			instr.setBadJump();
			context.cond.a = _REG.X;
			context.cond.b = _arg(instr, 0);
			return Base.nop();
		},
		cpy: function(instr, context) {
			instr.setBadJump();
			context.cond.a = _REG.Y;
			context.cond.b = _arg(instr, 0);
			return Base.nop();
		},
		dec: function(instr, context) {
			var p = _arg(instr, 0);
			context.cond.a = p;
			context.cond.b = '0';
			return Base.subtract(p, p, 1);
		},
		dex: function(instr, context) {
			context.cond.a = _REG.X;
			context.cond.b = '0';
			return Base.subtract(_REG.X, _REG.X, 1);
		},
		dey: function(instr, context) {
			context.cond.a = _REG.Y;
			context.cond.b = '0';
			return Base.subtract(_REG.Y, _REG.Y, 1);
		},
		eor: function(instr, context) {
			return _math(Base.xor, instr);
		},
		inc: function(instr, context) {
			var p = _arg(instr, 0);
			context.cond.a = p;
			context.cond.b = '0';
			return Base.add(p, p, 1);
		},
		inx: function(instr, context) {
			context.cond.a = _REG.X;
			context.cond.b = '0';
			return Base.add(_REG.X, _REG.X, 1);
		},
		iny: function(instr, context) {
			context.cond.a = _REG.Y;
			context.cond.b = '0';
			return Base.add(_REG.Y, _REG.Y, 1);
		},
		jmp: _nop,
		jsr: function(instr) {
			var p = instr.parsed.opd[0].token;
			if (p.startsWith('0x')) {
				p = Variable.functionPointer(p);
			}
			return Base.call(p);
		},
		lda: function(instr, context) {
			context.cond.a = _REG.accumulator;
			context.cond.b = '0';
			var o = instr.parsed.opd[0];
			if (o.token.startsWith('sym.') || o.immediate) {
				return Base.assign(_REG.accumulator, instr.parsed.opd[0].token.replace(/^sym\./, ''));
			}
			return _load(instr, _REG.accumulator, 16);
		},
		ldx: function(instr, context) {
			context.cond.a = _REG.X;
			context.cond.b = '0';
			var o = instr.parsed.opd[0];
			if (o.token.startsWith('sym.') || o.immediate) {
				return Base.assign(_REG.X, instr.parsed.opd[0].token.replace(/^sym\./, ''));
			}
			return _load(instr, _REG.X, 16);
		},
		ldy: function(instr, context) {
			context.cond.a = _REG.Y;
			context.cond.b = '0';
			var o = instr.parsed.opd[0];
			if (o.token.startsWith('sym.') || o.immediate) {
				return Base.assign(_REG.Y, instr.parsed.opd[0].token.replace(/^sym\./, ''));
			}
			return _load(instr, _REG.Y, 16);
		},
		lsr: function(instr) {
			return _math(Base.shift_right, instr, 1);
		},
		ora: function(instr, context) {
			return _math(Base.xor, instr);
		},
		pha: _nop,
		php: _nop,
		pla: _nop,
		plp: _nop,
		rol: function(instr) {
			return _math_rotate(Base.rotate_left, instr, 1);
		},
		ror: function(instr) {
			return _math_rotate(Base.rotate_right, instr, 1);
		},
		rti: function(instr) {
			return Base.return();
		},
		rts: function(instr) {
			return Base.return();
		},
		sbc: function(instr) {
			return _math(Base.subtract, instr);
		},
		sec: function(instr, context) {
			return Base.assign(_REG.carry, 1);
		},
		sed: function(instr, context) {
			return Base.assign(_REG.decimal, 1);
		},
		sei: function(instr, context) {
			return Base.assign(_REG.interrupt, 1);
		},
		sta: function(instr) {
			var o = instr.parsed.opd[0];
			if (o.token.startsWith('sym.') || o.immediate) {
				return Base.assign(instr.parsed.opd[0].token.replace(/^sym\./, ''), _REG.accumulator);
			}
			return _store(instr, _REG.accumulator, 16);
		},
		stx: function(instr) {
			var o = instr.parsed.opd[0];
			if (o.token.startsWith('sym.') || o.immediate) {
				return Base.assign(instr.parsed.opd[0].token.replace(/^sym\./, ''), _REG.X);
			}
			return _store(instr, _REG.X, 16);
		},
		sty: function(instr) {
			var o = instr.parsed.opd[0];
			if (o.token.startsWith('sym.') || o.immediate) {
				return Base.assign(instr.parsed.opd[0].token.replace(/^sym\./, ''), _REG.Y);
			}
			return _store(instr, _REG.Y, 16);
		},
		tax: function(instr, context) {
			context.cond.a = _REG.X;
			context.cond.b = '0';
			return Base.assign(_REG.X, _REG.accumulator);
		},
		tay: function(instr, context) {
			context.cond.a = _REG.Y;
			context.cond.b = '0';
			return Base.assign(_REG.Y, _REG.accumulator);
		},
		tsx: function(instr, context) {
			context.cond.a = _REG.X;
			context.cond.b = '0';
			return Base.assign(_REG.X, _REG.stackptr);
		},
		txs: function(instr, context) {
			return Base.assign(_REG.stackptr, _REG.X);
		},
		txa: function(instr, context) {
			context.cond.a = _REG.accumulator;
			context.cond.b = '0';
			return Base.assign(_REG.accumulator, _REG.X);
		},
		tya: function(instr, context) {
			context.cond.a = _REG.accumulator;
			context.cond.b = '0';
			return Base.assign(_REG.accumulator, _REG.Y);
		},
		nop: _nop,
		invalid: _nop
	},
	parse: function(assembly) {
		var tokens = assembly.trim().match(/^([\w]+) ?(#)?(\()?((0x[\da-fA-F]+)|([a-zA-Z0-9._]+))?(\))?(,([\w]+))?\)?$/);
		var opds = [];
		if (tokens[5] || tokens[6]) {
			var token = tokens[5] || tokens[6];
			if (tokens[3] == '(' && tokens[7] == ')' && tokens[9]) {
				token += ' + ' + tokens[9];
			}
			opds.push({
				immediate: tokens[2] == '#',
				pointer: tokens[3] == '(',
				token: token,
			});
		}
		if (tokens[9] && tokens[3] == '(' && tokens[7] != ')') {
			opds.push(tokens[9]);
		}
		return {
			mnem: tokens[1],
			opd: opds
		};
	},
	context: function() {
		return {
			cond: {
				a: _REG.accumulator,
				b: '?',
			}
		};
	},
	preanalisys: function(instructions, context) {},
	localvars: function(context) {
		return [];
	},
	globalvars: function(context) {
		return [];
	},
	arguments: function(context) {
		return [];
	},
	returns: function(context) {
		return 'void';
	}
};